[PATCH] http: use null prototype for headersDistinct/trailersDistinct
authorMatteo Collina <hello@matteocollina.com>
Thu, 19 Feb 2026 14:49:43 +0000 (15:49 +0100)
committerJérémy Lal <kapouer@melix.org>
Tue, 24 Mar 2026 21:11:25 +0000 (22:11 +0100)
Use { __proto__: null } instead of {} when initializing the
headersDistinct and trailersDistinct destination objects.

A plain {} inherits from Object.prototype, so when a __proto__
header is received, dest["__proto__"] resolves to Object.prototype
(truthy), causing _addHeaderLineDistinct to call .push() on it,
which throws an uncaught TypeError and crashes the process.

Ref: https://hackerone.com/reports/3560402
PR-URL: https://github.com/nodejs-private/node-private/pull/821
Refs: https://hackerone.com/reports/3560402
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
CVE-ID: CVE-2026-21710

Gbp-Pq: Topic sec
Gbp-Pq: Name 52-http-use-null-prototype-for-headersDistinct-trailersDistinct.patch

lib/_http_incoming.js
test/parallel/test-http-headers-distinct-proto.js [new file with mode: 0644]
test/parallel/test-http-multiple-headers.js

index 1dd04fdf3e4228b56bc8054f486b2175a8d17304..994a24f028c34333be07eb097facef132bd1d383 100644 (file)
@@ -128,7 +128,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', {
   __proto__: null,
   get: function() {
     if (!this[kHeadersDistinct]) {
-      this[kHeadersDistinct] = {};
+      this[kHeadersDistinct] = { __proto__: null };
 
       const src = this.rawHeaders;
       const dst = this[kHeadersDistinct];
@@ -168,7 +168,7 @@ ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', {
   __proto__: null,
   get: function() {
     if (!this[kTrailersDistinct]) {
-      this[kTrailersDistinct] = {};
+      this[kTrailersDistinct] = { __proto__: null };
 
       const src = this.rawTrailers;
       const dst = this[kTrailersDistinct];
diff --git a/test/parallel/test-http-headers-distinct-proto.js b/test/parallel/test-http-headers-distinct-proto.js
new file mode 100644 (file)
index 0000000..bd4cb82
--- /dev/null
@@ -0,0 +1,36 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+const net = require('net');
+
+// Regression test: sending a __proto__ header must not crash the server
+// when accessing req.headersDistinct or req.trailersDistinct.
+
+const server = http.createServer(common.mustCall((req, res) => {
+  const headers = req.headersDistinct;
+  assert.strictEqual(Object.getPrototypeOf(headers), null);
+  assert.deepStrictEqual(Object.getOwnPropertyDescriptor(headers, '__proto__').value, ['test']);
+  res.end();
+}));
+
+server.listen(0, common.mustCall(() => {
+  const port = server.address().port;
+
+  const client = net.connect(port, common.mustCall(() => {
+    client.write(
+      'GET / HTTP/1.1\r\n' +
+      'Host: localhost\r\n' +
+      '__proto__: test\r\n' +
+      'Connection: close\r\n' +
+      '\r\n',
+    );
+  }));
+
+  client.on('end', common.mustCall(() => {
+    server.close();
+  }));
+
+  client.resume();
+}));
index f9c654ba2f873024a4ed8eaec7ab4d23893215e5..bc49ba1e43dc4e76f22b5420f930d2c63526a6fd 100644 (file)
@@ -26,13 +26,13 @@ const server = createServer(
       host,
       'transfer-encoding': 'chunked'
     });
-    assert.deepStrictEqual(req.headersDistinct, {
+    assert.deepStrictEqual(req.headersDistinct, Object.assign({ __proto__: null }, {
       'connection': ['close'],
       'x-req-a': ['eee', 'fff', 'ggg', 'hhh'],
       'x-req-b': ['iii; jjj; kkk; lll'],
       'host': [host],
-      'transfer-encoding': ['chunked']
-    });
+      'transfer-encoding': ['chunked'],
+    }));
 
     req.on('end', function() {
       assert.deepStrictEqual(req.rawTrailers, [
@@ -45,7 +45,7 @@ const server = createServer(
       );
       assert.deepStrictEqual(
         req.trailersDistinct,
-        { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] }
+        Object.assign({ __proto__: null }, { 'x-req-x': ['xxx', 'yyy'], 'x-req-y': ['zzz; www'] })
       );
 
       res.setHeader('X-Res-a', 'AAA');
@@ -132,14 +132,14 @@ server.listen(0, common.mustCall(() => {
       'x-res-d': 'JJJ; KKK; LLL',
       'transfer-encoding': 'chunked'
     });
-    assert.deepStrictEqual(res.headersDistinct, {
+    assert.deepStrictEqual(res.headersDistinct, Object.assign({ __proto__: null }, {
       'x-res-a': [ 'AAA', 'BBB', 'CCC' ],
       'x-res-b': [ 'DDD; EEE; FFF; GGG' ],
       'connection': [ 'close' ],
       'x-res-c': [ 'HHH', 'III' ],
       'x-res-d': [ 'JJJ; KKK; LLL' ],
-      'transfer-encoding': [ 'chunked' ]
-    });
+      'transfer-encoding': [ 'chunked' ],
+    }));
 
     res.on('end', function() {
       assert.deepStrictEqual(res.rawTrailers, [
@@ -153,7 +153,7 @@ server.listen(0, common.mustCall(() => {
       );
       assert.deepStrictEqual(
         res.trailersDistinct,
-        { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] }
+        Object.assign({ __proto__: null }, { 'x-res-x': ['XXX', 'YYY'], 'x-res-y': ['ZZZ; WWW'] })
       );
       server.close();
     });